﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using System.Diagnostics;
using System.Windows.Forms;
using System.Runtime.Serialization.Formatters;
using System.Runtime.Serialization;
using System.Reflection;

namespace PluginSurrogate
{
    public delegate object BrokerAction(IWin32Window owner, object input);

    public class SurrogateHelper
    {
        /// <summary>
        /// Start a new process using command and arguments, while
        /// simulating modality in the owner.
        /// </summary>
        /// <param name="owner">The owner window to be modal to. If
        /// modality is not necessary, use Execute instead.</param>
        /// <param name="input">An object to pass to the new process
        /// on its standard input.</param>
        /// <param name="command">The executable file to run.</param>
        /// <param name="arguments">Command line arguments to the
        /// executable.</param>
        /// <returns>An object printed by the process on its standard
        /// output.</returns>
        public static object ExecuteModally(IWin32Window owner, object input, string command, string arguments)
        {
            try
            {
                object output = null;
                HwndAndData data = new HwndAndData { Hwnd = owner.Handle.ToInt64(), Data = input };
                SimulateModality.ExecuteModally(owner, delegate
                {
                    output = Execute(data, command, arguments);
                });
                return output;
            }
            catch (Exception e)
            {
                Trace.Fail(e.ToString());
                return null;
            }
        }

        public static object Execute(object input, string command, string arguments)
        {
            if (input != null && !input.GetType().IsSerializable)
                throw new ArgumentException("Input type " + input.GetType().Name + " is not serializable");

            Process p = new Process();
            p.StartInfo = new ProcessStartInfo(command, arguments)
            {
                RedirectStandardInput = true,
                RedirectStandardOutput = true,
                UseShellExecute = false
            };
            p.Start();
            BinaryFormatter formatter = CreateFormatter();

            using (p.StandardInput)
            {
                if (input != null)
                {
                    formatter.Serialize(p.StandardInput.BaseStream, input);
                }
            }

            switch ((byte) p.StandardOutput.BaseStream.ReadByte())
            {
                case (byte)ReturnType.Normal:
                    return formatter.Deserialize(p.StandardOutput.BaseStream);
                case (byte)ReturnType.Exception:
                    throw (Exception)formatter.Deserialize(p.StandardOutput.BaseStream);
                default:
                    throw new ArgumentException("Unexpected stream format returned from surrogate");
            }
        }

        public static void PerformOperation(BrokerAction action)
        {
            HwndAndData hnd = (HwndAndData)ReadInput();
            try
            {
                WriteOutput(action(new Win32Window(new IntPtr(hnd.Hwnd)), hnd.Data));
            }
            catch (Exception e)
            {
                WriteException(e);
            }
        }

        [Serializable]
        public class HwndAndData
        {
            public long Hwnd { get; set; }
            public object Data { get; set; }
        }

        private const string NULL_TOKEN = "{B6559C22-C182-421f-8133-28196B722FD3}";
        private enum ReturnType : byte { Normal = 30, Exception }

        private static object ReadInput()
        {
            BinaryFormatter formatter = CreateFormatter();
            object obj;
            using (Stream s = Console.OpenStandardInput())
            {
                obj = formatter.Deserialize(s);
            }
            if (obj is string && obj.Equals(NULL_TOKEN))
                return null;
            else
                return obj;
        }

        private static void WriteOutput(object obj)
        {
            if (obj != null && !obj.GetType().IsSerializable)
                throw new ArgumentException("Object type " + obj.GetType().Name + " is not serializable");

            if (obj == null)
                obj = NULL_TOKEN;

            BinaryFormatter formatter = CreateFormatter();
            using (Stream s = Console.OpenStandardOutput())
            {
                s.WriteByte((byte)ReturnType.Normal);
                formatter.Serialize(s, obj);
            }
        }

        private static void WriteException(Exception e)
        {
            if (!e.GetType().IsSerializable)
            {
                e = new Exception(e.Message);
            }

            BinaryFormatter formatter = CreateFormatter();
            using (Stream s = Console.OpenStandardOutput())
            {
                s.WriteByte((byte)ReturnType.Exception);
                formatter.Serialize(s, e);
            }
        }

        private static BinaryFormatter CreateFormatter()
        {
            BinaryFormatter formatter = new BinaryFormatter();
            formatter.Binder = new CustomBinder();
            return formatter;
        }

        /// <summary>
        /// A custom binder is necessary because .NET serialization doesn't
        /// seem to pick up assemblies that are in the private bin path
        /// </summary>
        private class CustomBinder : SerializationBinder
        {
            public override Type BindToType(string assemblyName, string typeName)
            {
                // If this is our assembly, manually get the type; otherwise use
                // default behavior (at least what I assume is the default behavior)
                if (assemblyName == typeof(CustomBinder).Assembly.FullName)
                    return typeof(CustomBinder).Assembly.GetType(typeName, true);
                else
                    return Type.GetType(Assembly.CreateQualifiedName(assemblyName, typeName));
            }
        }

        private class Win32Window : IWin32Window
        {
            private IntPtr handle;

            public Win32Window(IntPtr handle)
            {
                this.handle = handle;
            }

            public IntPtr Handle
            {
                get { return handle; }
            }
        }
    }
}
